Skip to content

Add edge-case tests for weighted diameter; document asymmetric-weight bug#1

Open
Krastanov-agent wants to merge 2 commits intoLoveLow-Global:optimize-diameter-2from
Krastanov-agent:pr495-edge-case-tests
Open

Add edge-case tests for weighted diameter; document asymmetric-weight bug#1
Krastanov-agent wants to merge 2 commits intoLoveLow-Global:optimize-diameter-2from
Krastanov-agent:pr495-edge-case-tests

Conversation

@Krastanov-agent
Copy link

Summary

This PR adds a comprehensive edge-case test suite (test/test_diameter_edge_cases.jl) for the weighted diameter algorithm introduced in JuliaGraphs#495. It includes 50 targeted test cases and one bug report found through systematic testing.

Test results: 180 pass, 1 fail

The 1 failure is an intentional test that demonstrates a bug (see below).

Bug Found: Asymmetric Weight Matrix on Undirected Graphs

Description

_diameter_weighted_undirected produces wrong results when the weight matrix is not symmetric (distmx[i,j] != distmx[j,i]).

Minimal reproducer

g = path_graph(4)  # undirected: 1-2-3-4
distmx = [0.0 1.0 Inf Inf;
           5.0 0.0 1.0 Inf;
           Inf 5.0 0.0 1.0;
           Inf Inf 5.0 0.0]

diameter(g, distmx)                                     # => 5.0  (WRONG)
maximum(eccentricity(g, vertices(g), distmx))           # => 15.0 (correct)

Root cause analysis

The iFUB stopping condition at src/distance.jl:330:

lb >= 2 * d_prev && break

relies on the bound: for any unprocessed vertex v with d(hub, v) ≤ d_prev, the eccentricity satisfies ecc(v) ≤ 2 * d_prev. This comes from the triangle inequality:

d(v, w) ≤ d(v, hub) + d(hub, w) ≤ d_prev + d_prev = 2 * d_prev

The problem: the algorithm only computes d(hub, v) (forward Dijkstra from hub), but the bound requires d(v, hub) (the reverse direction). For undirected graphs with symmetric weights, d(v, hub) = d(hub, v), so the bound holds. But when distmx[i,j] ≠ distmx[j,i], the two distances diverge and the bound becomes invalid.

Detailed trace of the bug

Hub = vertex 2 (highest degree in path_graph(4))

Dijkstra from hub=2:
  d(hub→1) = 5.0,  d(hub→3) = 1.0,  d(hub→4) = 2.0
  → lb = 5.0
  → Fringes: d=5.0 → [v=1],  d=2.0 → [v=4],  d=1.0 → [v=3]

Processing outermost fringe d_i=5.0, d_prev=2.0:
  Dijkstra from v=1: ecc(1) = 3.0  →  lb = max(5.0, 3.0) = 5.0
  Check: lb=5.0 ≥ 2*d_prev=4.0?  YES → BREAK!

But vertex 4 (in fringe d=2.0, never processed) has ecc(4) = 15.0!
  d(hub→4) = 2.0 (forward, what the algorithm sees)
  d(4→hub) = 10.0 (backward, what the bound needs: 4→3→2 costs 5+5)

Impact

  • Failure rate: ~77% with random asymmetric weights on undirected graphs (tested on 3000 random graphs)
  • Regression: the old code maximum(eccentricity(g, distmx)) handled this correctly
  • No impact on symmetric weights: 7000+ random tests with symmetric weight matrices all pass
  • No impact on directed graphs: the directed algorithm (_diameter_weighted_directed) correctly tracks both forward and backward distances via separate fringes, so it handles asymmetric weights properly

Possible fixes

  1. Symmetrize on entry: distmx = (distmx + distmx') / 2 — changes semantics
  2. Use the directed algorithm for undirected graphs when distmx ≠ distmx' — correct but slower
  3. Track backward distances too in the undirected algorithm (like the directed one does) — most robust
  4. Document the restriction that distmx must be symmetric for undirected graphs — least code change

Minor observation: >= vs > in breaking condition

  • Undirected (distance.jl:330): lb >= 2 * d_prev
  • Directed (distance.jl:290): lb > 2 * d_prev

The directed > is slightly more conservative (processes one extra fringe). Testing confirms both >= and > produce correct results for directed graphs (1948 additional tests), so this is just a minor inefficiency, not a correctness bug. Using >= for both would be fine.

Test cases included (50 tests, 181 assertions)

# Test Type
1 Empty graph (0 vertices) Edge case
2 Single vertex Edge case
3 Two vertices, undirected edge Edge case
4 Two vertices, directed cycle Edge case
5 Two vertices, one directed edge (disconnected) Edge case
6 Disconnected undirected Edge case
7 No edges, multiple vertices Edge case
8 Path graph undirected weighted Known diameter
9 Path digraph weighted (not strongly connected) Edge case
10 Cycle graph undirected weighted Known diameter
11 Cycle digraph weighted Known diameter
12 Star graph undirected weighted Known diameter
13 Complete graph undirected weighted vs naive
14 Complete digraph directed weighted vs naive
15 Unit weight matrix = unweighted diameter Consistency
16 Diameter between low-degree peripheral vertices Structure
17 Integer weight matrix Type variant
18 Large weights (1e15) Numerics
19 Very small weights (1e-15) Numerics
20 Uniform edge weights Consistency
21 Directed cycle with asymmetric weights Directed
22 Complete tournament (directed) vs naive
23 Grid graph weighted vs naive
24 Binary tree weighted vs naive
25 Barbell graph weighted vs naive
26 Directed graph with detour Directed
27 Wheel graph weighted vs naive
28 Caterpillar graph weighted vs naive
29 Regular graph (cycle) weighted vs naive
30 Petersen graph weighted vs naive
31 Barabasi-Albert weighted vs naive
32 Random ER undirected (30 samples) Stress
33 Random ER directed (30 samples) Stress
34 Disconnected random undirected Edge case
35 Disconnected random directed Edge case
36 Directed: diameter found in reverse Directed
37 Hub far from diameter endpoints Structure
38 Float32 weight matrix Type variant
39 Zero-weight edges Edge case
40 Bridge edge with high weight Structure
41 Directed regular graph weighted vs naive
42 Dense random undirected vs naive
43 Sparse random undirected (tree-like) vs naive
44 Initial lower bound equals diameter Edge case
45 Type inference Type stability
46 Asymmetric weights on undirected graph BUG
47 Mixed directed graph vs naive
48 Diameter pair both adjacent to hub Structure
49 Directed graph with multiple SCCs Edge case
50 Stress test random graphs (20 seeds) Stress

Test plan

  • Run test/test_diameter_edge_cases.jl — 180 pass, 1 expected fail (asymmetric-weight bug)
  • Run existing test/distance.jl — all 280 tests pass
  • Stress tested 7000+ random graphs with symmetric weights — all pass
  • Stress tested 1948 directed graphs with >= vs > condition — both correct

🤖 Generated with Claude Code

… bug

Add 50 edge-case tests for the weighted diameter algorithm covering:
- Empty/single-vertex/two-vertex graphs
- Path, cycle, star, complete, grid, Petersen, wheel, barbell, caterpillar, tree graphs
- Directed and undirected variants
- Integer, Float32, Float64 weight matrices
- Very large/small/zero weights
- Disconnected graphs (should return Inf/typemax)
- Hub far from diameter endpoints
- Dense and sparse random graphs
- Stress tests (30+ random ER seeds for each of directed/undirected)

One test demonstrates a bug: _diameter_weighted_undirected returns
wrong results with asymmetric weight matrices (distmx[i,j] != distmx[j,i]).
The iFUB bound "lb >= 2*d_prev" assumes d(v,hub)=d(hub,v), which fails
when the weight matrix is not symmetric. Failure rate ~77% on random inputs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two fixes:

1. _diameter_weighted_undirected: track both forward and backward
   distance fringes from the hub vertex, using the transposed weight
   matrix for backward distances. The iFUB stopping bound
   "lb >= 2*d_prev" requires that for any unprocessed vertex v,
   BOTH d(hub,v) and d(v,hub) are <= d_prev. With asymmetric weights
   (distmx[i,j] != distmx[j,i]), d(v,hub) can greatly exceed d(hub,v),
   invalidating the bound and causing the algorithm to return wrong
   results (~77% failure rate on random asymmetric weights).

   The fix mirrors the directed algorithm's approach: compute Dijkstra
   from the hub with both distmx and permutedims(distmx), group
   vertices into forward and backward fringes, and process both.
   For fringe_fwd vertices, compute in-eccentricity (Dijkstra with
   transposed weights); for fringe_bwd, compute out-eccentricity
   (Dijkstra with original weights).

2. _diameter_weighted_directed: change breaking condition from
   "lb > 2*d_prev" to "lb >= 2*d_prev" for consistency with the
   undirected case. The >= condition is sufficient because when
   lb == 2*d_prev, all remaining vertex eccentricities are bounded
   by 2*d_prev <= lb, so no further processing is needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Krastanov
Copy link

The suggested solution here might be stupid. Instead of doubling the number of checks, maybe early one we should be checking for asymmetry.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants